/*
 * Created on Oct 17, 2004
 *
 * TODO To change the template for this generated file go to
 * Window - Preferences - Java - Code Style - Code Templates
 */
package jxta.chat.voice;


import net.jxta.endpoint.ByteArrayMessageElement;
import net.jxta.endpoint.StringMessageElement;
import net.jxta.logging.Logging;
import net.jxta.myjxta.dialog.Dialog;
import net.jxta.myjxta.dialog.DialogMessage;

import javax.sound.sampled.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @author Ravi
 * @author jamoore
 * @modified 2005-03-05 jamoore add producer/consumer threads, line state
 * @modified 2005-03-05 jamoore audio encoding is now dynamic based on quality
 * @modified 2005-03-10 jamoore rework encoding to fit dynamic block sizes
 * @modified 2005-03-12 jamoore add statistical accessors
 * @modified 2005-03-18 jamoore refactor
 * @modified 2005-03-18 jamoore add in/out buffer size/capacity accessor/stats
 * 
 */
public final class VoiceMicrophoneInput extends Thread implements AudioResource{
    
    static final Logger LOG = Logger.getLogger (VoiceMicrophoneInput.class.getName());
    
    /** default number of speex encoded chunks to place in each message*/
    private static final int DEFAULT_MESSAGE_SIZE_MULTIPLIER = 10;
    
    /** default number of speex encoded chunks the out going buffer will hold*/
    private static final int DEFAULT_BUFFER_SIZE_MULTIPLIER = 50;
    
    /** these represetn the logical state of the hardware to the call control*/
    private static final int STATE_ON = 20;
    
    private static final int STATE_OFF = 10;
    
    private static final int STATE_PAUSE = 30;
    
    /** represents the current state of the audio targetline */
    private int micState = STATE_PAUSE;
    
    /** type of audio we want from the target line */
    private final AudioFileFormat.Type targetType = null;

    /** data line to microphone*/
    private TargetDataLine targetLine = null;;
    
    /** wrapper to the speex encoder */
    private Encoder encoder = null;;
    
    /** call control object - performs protocol navigation */
    private VoJxtaCallControl vojxtaCallControl = null;
    
    /** outgoing buffer that holds que'd up speex audio blocks */
    private VoiceDataBuffer sendBuffer = null;
    
    /** wait lock to pause the target line from reading*/
    private Object pauseLock = null;
    
    /** wait lock till the buffer recovers from starvation*/
    private Object messageThreadLock = null;
    
    /** dialog that we dispatch messages to*/
    //public Dialog vojxtaDialog = null;
    
    // debugging
    boolean firstSentMessage = true;
    byte[] firstSentBytes = null;
    
    /** statistic*/
    private long sentMessages = 0;

    /** statistic*/
    private long sentBytes = 0;
    
    /** true if message thread is waiting for the buffer to fill up*/
    private boolean messageThreadWaiting = false;
    
    /** size of audio chucks speex can encode at the current sample rate*/
    private final int rawChunkSize = BLOCK_SIZE; // basically at the sample rate this is 640 bytes
    
    /** size of the speex encoded audio. this depends on the quality setting*/
    private int speexChunkSize = 0;;
    
    /** the size in bytes each outgoing message contains*/
    private int speexMessageSize = 0;
    
    /** size in bytes of the outgoing buffer holding speex encoded bytes*/
    private int speexBufferSize = 0;
    
    /** target line buffer size*/
    private final int rawBufferSize = rawChunkSize * 10;
    
    /** size in bytes of buffer read from target Line */
    private final int audioReadChunkSize = rawChunkSize * 5;
    
    /** default mixer on target line */
    private Mixer micMixer = null;
    
    /** default gain control on target line*/
    private FloatControl gainControl = null;
    
    /** statistic */
    private int averageEncodeTime = 0;
    
    /** speex quality level */
    private int encodeQuality = 0;
    
    /** consumer thread, dispatches messages to dialog*/
    private Thread messageThread = null;
    
    /**
     * 
     */
    public VoiceMicrophoneInput (VoJxtaCallControl vojxtaCallControl) {
        
        LOG.setLevel (Level.INFO);
        //LOG.setLevel (Level.SEVERE);
        pauseLock = new Object ();
        
        messageThreadLock = new Object ();
        
        this.vojxtaCallControl = vojxtaCallControl;
        
        //this.vojxtaDialog = this.vojxtaCallControl.getDialog ();
        
        /** This thread dispatches messages to the vojxta dialog and  
         *  blocks on outgoing buffer starvation */
        messageThread = new Thread (new Runnable () {
            
            public void run () {
    
                while(true) {

                    if(getMicState () == STATE_OFF) {
                        
                        //we should send any remaining data in buffer first
                        break;
                    }
                    
                    if (sendBuffer.size () >= speexMessageSize) {
                        
                        dispatchVoiceData (sendBuffer.get (speexMessageSize));
                    }else {
                        
                        try {
                            
                            synchronized(messageThreadLock) {
                                
                                messageThreadWaiting = true;
                                
                                messageThreadLock.wait ();
                            }
                        }catch(InterruptedException ix) {
                            
                            ix.printStackTrace ();
                        }
                    }
                }
    
            }
        });
    }//constructor
    
    /**
     *  Retain the mic line from the audiosubsystem. Try for a gain control too.
     *  For some raeson target lines do not have available controls. we might have
     * to add some amplitude to the line oourselve.
     */
    public void obtainHardware () {
        
        AudioFormat  audioFormat = new AudioFormat (AudioFormat.Encoding.PCM_SIGNED, 16000.0F, 16, 1, 2, 16000.0F, false);
        
        DataLine.Info info = new DataLine.Info (TargetDataLine.class, audioFormat);
        
        try {
            
            targetLine = (TargetDataLine) AudioSystem.getLine (info);
            
            targetLine.open (audioFormat, rawBufferSize );
            
        } catch (LineUnavailableException e) {
            // TODO Auto-generated catch block
            e.printStackTrace ();
            
        }
        
        AudioFileFormat.Type targetType = AudioFileFormat.Type.WAVE;
        
        if(targetLine.isControlSupported (FloatControl.Type.MASTER_GAIN)) {
            
            gainControl = (FloatControl)targetLine.getControl (FloatControl.Type.MASTER_GAIN);
        }
        
    }
    
    
    /**
     * Return block size of encoded audio bytes
     */
    public int getEncodedBlockSize () {
        
        return speexChunkSize;
    }
    
    /**
     *  Return the size in byte of voice data in a message
     */
    public int getEncodedMessageSize () {
        
        return speexMessageSize;
        
    }
    
    /**
     *  Set the size in bytes of voice data in a message
     */
    public void setEncodedMessageSize (int encodedMessageSize) {
        
        this.speexMessageSize = encodedMessageSize;
    }
    
    /**
     *  Returns the size in bytes of the buffer holding outgoing encoded voice
     *  bytes
     */
    public int getEncodedBufferSize () {
        
        return speexBufferSize;
    }
    
    /**
     *  Sets the size in bytes of the buffer that holds encoded voice bytes. 
     *  The current buffer impl does not allow dynamic resizing
     */
    public void setEncodedBufferSize (int encodedBufferSize) {
        
        this.speexBufferSize = encodedBufferSize;
        
    }
    
    /* 
     *  producer thread. read from target line blocks till buffer size can be 
     *  filled. data is encoded then added to the outgoing buffer.
     */
    public void run () {
        
        /** start the message dispatch thread*/
        messageThread.start ();
        
        /** local buff to read raw audio into */
        byte[] buff = new byte[audioReadChunkSize];
        /** bytes read from targetline... if zero after read that mean the line
            has been stopped */
        int bytesRead = 0;
        
        try {
            
            while((bytesRead = 
                        this.targetLine.read (buff,0,buff.length)) != -1) {
                
                if(getMicState () == this.STATE_PAUSE) {
                    
                    synchronized(pauseLock) {
                        
                        pauseLock.wait ();
                    }
                }
                
                if(getMicState () == this.STATE_OFF) {
                    
                    //we should send any remaining data in buffer first
                    break;
                }

                if(bytesRead > 0) {
                    
                    encodeAndStore (buff);
                    
                    if (messageThreadWaiting && sendBuffer.size () >= speexMessageSize) {
                        
                        messageThreadWaiting = false;

                        synchronized(messageThreadLock) {
                            
                            messageThreadLock.notify ();
                        }
                    }
                }else{
                    /** if bytes read is zero something is wrong.. break */
                   if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                       
                        this.printMicState ();
                        
                        if(!targetLine.isOpen ()) {
                            LOG.info("targetline != open");
                        }
                        
                        LOG.info("ZERO Bytes READ");
                        
                        break;
                    }
                }
            }//while
        }catch (InterruptedException ix ) {
            
            setMicState (this.STATE_OFF);
            
            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("run : interuptedexception");
            }
            
            ix.printStackTrace ();
            
        }
        
    }//run
    
    
    /**
     *  encodes the raw pcm audio from target line then stores it in the 
     *  outgoing buffer. at the current sample rate speex encodes 640byte
     *  blocks. we break the buff up into 640byte chunks -> encode -> store
     */
    private void encodeAndStore (byte[] buff) {
        
        
        int chunks = buff.length / rawChunkSize;
        
        int[] startPos = new int[chunks];
        
        for(int j = 0; j < chunks; j++) {
            
            startPos[j] = j * rawChunkSize;
        }
        
        for(int i = 0 ; i < chunks ; i++) {
            
            byte[] preEncodeBuff = new byte[rawChunkSize];
            
            System.arraycopy (buff,startPos[i], preEncodeBuff, 0, rawChunkSize );
            
            long in = System.currentTimeMillis ();
            
            byte[] postEncodeBuff = encoder.encode (preEncodeBuff);
            
            long out = System.currentTimeMillis ();
            
            int roundTrip = (int)(out - in);
            
            if(averageEncodeTime != 0) {

                averageEncodeTime = (int) (averageEncodeTime + roundTrip) / 2;
            }else{
                
                averageEncodeTime = roundTrip;
            }
           
            sendBuffer.append (postEncodeBuff);
            
        }
    }
    
    /**
     * Returns the current size in bytes of data stored in the send buffer
     */
    public int getBufferSize() {
        
        return sendBuffer.size();
    }
    
    /**
     * Returns the size in bytes of the capacity of the send buffer
     */
    public int getBufferCapacity() {
        
        return sendBuffer.getCapacity();
    }
    
    public int getAverageEncodeTime () {
        
        return averageEncodeTime;
    }
    
    public int getEncodeQuality () {
        return this.encodeQuality;
    }
    
    protected VoJxtaCallControl getCallControl () {
        
        return this.vojxtaCallControl;
        
    }
    
    public int getAudioBlockSize () {
        return this.rawChunkSize;
    }

    /**
     * Encapsulates audio data and session command into a dialogMessage.
     * Diapatches to remote peer.
     */
    public void dispatchVoiceData (byte[] speexData) {
        
        // for testing purposes
        // check if encode/decode process is stable
        if(firstSentMessage) {
            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("dispatchVoiceData : firstSentMessage");
            }
            if(speexData!=null) {
                firstSentBytes = new byte[speexData.length];
                System.arraycopy (speexData, 0 , firstSentBytes, 0 ,  speexData.length);
                firstSentMessage = false;
                
            }else{
                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("dispatchVoiceData : voice data is null");
                }
            }
        }
        
        
        
        
        DialogMessage msg = (DialogMessage) getCallControl ().getTemplateMessage ().clone ();
        
        StringMessageElement commandElement = new StringMessageElement (
                VoJxtaCallControl.TAG_SESSION_COMMAND, VoJxtaCallControl.COMMAND_VOJXTA_DATA, null);
        
        msg.addMessageElement (VoJxtaCallControl.TAG_SESSION_COMMAND, commandElement);
        
        
        ByteArrayMessageElement voiceElement = new ByteArrayMessageElement (
                VoJxtaCallControl.TAG_VOICE_DATA, null, speexData, 0, null);
        
        msg.addMessageElement (VoJxtaCallControl.TAG_VOICE_DATA, voiceElement);
        
        //this.vojxtaDialog.dispatch (msg);
        
        this.sentMessages=this.sentMessages+1;
        
        this.sentBytes=this.sentBytes+speexData.length;
    }
    
    
    public long getNumberOfBytesSent () {
        return this.sentBytes;
    }
    
    public long getNumberOfMessagesSent () {
        
        return sentMessages;
        
    }
    
    
    private int getMicState () {
       
        return this.micState;
    }
    
    private void setMicState (int state) {

        this.micState = state;
    }
    

        /**
     *  Starts the tagetLine. sets up the buffer and message size.
     */
    public void beginMic () {
        
        encodeQuality = getCallControl ().getMinimumVoiceQuality ();
        
        encoder = new Encoder (getCallControl ().getMinimumVoiceQuality ());
        
        speexChunkSize = ((Integer)VoJxtaCallControl.qualityByteMap.get (new Integer(getCallControl ().getMinimumVoiceQuality ()))).intValue ();
        LOG.info("mic chunk size " + speexChunkSize);
        if ( speexMessageSize == 0) {
            
            speexMessageSize = speexChunkSize * DEFAULT_MESSAGE_SIZE_MULTIPLIER;
        }
        LOG.info("mic message size " + speexMessageSize);
        if (speexBufferSize == 0) {
            
            speexBufferSize = speexChunkSize * DEFAULT_BUFFER_SIZE_MULTIPLIER;
        }
        LOG.info("mic buffer size " + speexBufferSize);
        this.sendBuffer = new VoiceDataBuffer (speexBufferSize, this, "MicControl");
        
        if(getMicState () == STATE_PAUSE) {
            
            setMicState (this.STATE_ON);
            
            synchronized(pauseLock) {
                
                pauseLock.notify ();
            }
            
            synchronized(messageThreadLock) {
                
                messageThreadLock.notify ();
            }
        }
        
        this.targetLine.start ();
        
        super.start ();
    }
    
    
    
    /**
     *   Disallows reading to the target line (mic). sets thread state to
     *   off
     */
    public void endMic () {
        
        setMicState (this.STATE_OFF);
        
        if(targetLine != null) {
            this.targetLine.stop ();
        }
    }
    
    
    /**
     *  Halts the reading of data to this targetline. pauses audio line read
     *  thread.
     */
    public void pauseMic () {
        
        setMicState (this.STATE_PAUSE);
        
        this.targetLine.stop ();
    }
    
    /**
     *  Restarts the target line and message send thread.
     */
    public void resumeMic () {
        
        this.targetLine.start ();
        
        setMicState (this.STATE_ON);
        
        synchronized(pauseLock) {
            pauseLock.notify ();
        }
        
        synchronized(messageThreadLock) {
            messageThreadLock.notify ();
        }
        
    }
    
    
    /**
     *  Releases hardware resources. tagetline, mixers and controls.
     */
    public void releaseHardware () {
        
        if(targetLine != null) {
        targetLine.flush();
        
        targetLine.stop ();
        
        targetLine.close ();
        }
    }
    
    
    /**
     *  Return true if gain is supported in line target line.
     */
    public boolean isGainControlSupported () {
        
        return gainControl != null;
    }
    
    public float getMaxGainValue () {
        
        return (gainControl!=null) ? gainControl.getMaximum () : 0.0f;
    }
    
    public float getMinGainValue () {
        
        return (gainControl!=null) ? gainControl.getMinimum () : 0.0f;
    }
    
    public String getGainUnits () {

        return (gainControl!=null) ?  gainControl.getUnits () : "";
    }

    public String getMaxLabel () {

        return (gainControl!=null) ? gainControl.getMaxLabel () : "";
    }

    public String getMinLabel () {

        return (gainControl!=null) ? gainControl.getMinLabel () : "";
    }
    
    public float getGainValue () {

        return (gainControl!=null) ? gainControl.getValue () : 0.0f;
    }
    
    public void adjustGainControl (float newValue) {
        
        if(gainControl!=null) {
            
            gainControl.setValue (newValue);
        }
        
    }

    

    private void printMicState () {
        
        String s = null;
        
        if(this.micState == STATE_PAUSE) { s = "PAUSE  "; }
        
        if(this.micState == STATE_ON) { s = "ON  "; }
        
        if(this.micState == STATE_OFF) { s = "OFF  "; }
        
        
        
    }
        private void printMixers () {
        System.out.println ("\n\rMixers : ");
        Mixer.Info[] info = AudioSystem.getMixerInfo ();
        for(int i = 0; i< info.length; i++) {
            
            System.out.println ("\tName: "+info[i].getName ()+ " Description: "+info[i].getDescription ());
            
        }
        
    }
    
    private void printControls () {
        System.out.println ("\n\rControls : ");
        Control[] controls = targetLine.getControls ();
        
        for(int i = 0; i< controls.length; i++) {
            
            System.out.println ("\tName: "+controls[i].toString ());
            
        }
        
        
    }

    
    
    protected void printSupportedControls () {
        LOG.info("Mic SupportedControls");
        LOG.info("mute " +
                 targetLine.isControlSupported(javax.sound.sampled.BooleanControl.Type.MUTE));
        LOG.info("Balance " +
                 targetLine.isControlSupported(FloatControl.Type.BALANCE));
        LOG.info("MasterGain " +
                 targetLine.isControlSupported(FloatControl.Type.MASTER_GAIN));
        LOG.info("Pan " + targetLine.isControlSupported(FloatControl.Type.PAN));
        LOG.info("SampleRate " +
                 targetLine.isControlSupported(FloatControl.Type.SAMPLE_RATE));
        LOG.info("Volume " +
                 targetLine.isControlSupported(FloatControl.Type.VOLUME));
        
        
        Mixer.Info[] mixerInfo = AudioSystem.getMixerInfo ();
        for (int i = 0; i < mixerInfo.length; i++) {
            micMixer = AudioSystem.getMixer(mixerInfo[i]);
            LOG.info("MicMixer SupportedControls for mixer" +
                     mixerInfo[i].toString());
            LOG.info("mute " +
                     micMixer.isControlSupported(javax.sound.sampled.BooleanControl.Type.MUTE));
            LOG.info("Balance " +
                     micMixer.isControlSupported(FloatControl.Type.BALANCE));
            LOG.info("MasterGain " +
                     micMixer.isControlSupported(FloatControl.Type.MASTER_GAIN));
            LOG.info("Pan " + micMixer.isControlSupported(FloatControl.Type.PAN));
            LOG.info("SampleRate " +
                     micMixer.isControlSupported(FloatControl.Type.SAMPLE_RATE));
            LOG.info("Volume " +
                     micMixer.isControlSupported(FloatControl.Type.VOLUME));
        }
        
    }
    
}
